﻿using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;

using System.Net.DnsToolkit.Packet.Records;
using System.Net.DnsToolkit.Packet;

namespace System.Net.DnsToolkit.Serializer.Stream.Compression {

    /// <summary>
    /// TODO: Refactor using IoC
    /// </summary>
    public static class CompressionHelper {

        public static void ForEach(this SubDomainName subDomain, Action<SubDomainName> action) {

            if (subDomain == null) {
                throw new ArgumentNullException("subDomain");
            }
            if (action == null) {
                throw new ArgumentNullException("action");
            }

            var domains = new List<SubDomainName>();
            SubDomainName domn = subDomain;
            do {

                action.Invoke(domn);
                domains.Add(domn);
                domn = domn.Right;

            } while (domn != null
                     && !domains.Contains(domn));

        }

        /// <summary>
        /// Get full domain name from a subDomain part
        /// </summary>
        /// <param name="subDomain"></param>
        /// <returns></returns>
        public static string FullDomain(this SubDomainName subDomain) {

            if (subDomain == null) {
                throw new ArgumentNullException("subDomain");
            }

            var sb = new StringBuilder();
            subDomain.ForEach(domn => sb.Append(domn.Name));
            return sb.ToString();

        }

        /// <summary>
        /// Extracts compressible domain names from DNS message
        /// </summary>
        /// <param name="message"></param>
        /// <param name="domains"></param>
        /// <returns></returns>
        public static void CreateDomain(IMessage message, List<IDomainName> domains) {

            if (message == null) {
                throw new ArgumentNullException("message");
            }

            if (domains == null) {
                throw new ArgumentNullException("domains");
            }

            var qnames = from q in message.Questions
                         select q.QName ;

            IEnumerable<IRR> rrs = message.Additionals
                .Union(message.Authorities)
                .Union(message.Answers);

            var rnames = from rr in rrs
                         select rr.Name;

            var rdatas = rrs
                .SelectMany(rr => CreateDomain(rr))
                .Where(rr => rr.Name != null);

            IEnumerable<IDomainName> domns = qnames
                .Union(rnames)
                .Union(rdatas);

            domains.AddRange(domns);

        }

        private static IDomainName[] CreateDomain(IRR rr) {

            if (rr == null) {
                throw new ArgumentNullException("rr");
            }

            IDomainName[] domn;
            IRecord record = rr.RData;
            if (record is IRecordCNAME) {
                domn = CreateDomain((IRecordCNAME)rr.RData);
            } else if (record is IRecordMX) {
                domn = CreateDomain((IRecordMX)rr.RData);
            } else if (record is IRecordSOA) {
                domn = CreateDomain((IRecordSOA)rr.RData);
            } else if (record is IRecordNS) {
                domn = CreateDomain((IRecordNS)rr.RData);
            } else if (record is IRecordPTR) {
                domn = CreateDomain((IRecordPTR)rr.RData);
            } else {
                domn = new IDomainName[] { };
            }
            return domn;
            
        }
        private static IDomainName[] CreateDomain(IRecordMX recordMX) {
            return new[] { recordMX.EXCHANGE };
        }
        private static IDomainName[] CreateDomain(IRecordPTR recordPTR) {
            return new[] { recordPTR.PTRDNAME };
        }

        private static IDomainName[] CreateDomain(IRecordCNAME recordCNAME) {
            return new[] { recordCNAME.CNAME };
        }

        private static IDomainName[] CreateDomain(IRecordNS recordNS) {
            return new[] { recordNS.NSDNAME };
        }

        private static IDomainName[] CreateDomain(IRecordSOA recordSOA) {
            return new[] { recordSOA.RNAME, recordSOA.MNAME };
        }

        private static readonly Regex Subdomain = new Regex(@"(?<SubDomain>[^\.]+\.)$", RegexOptions.Compiled);

        /// <summary>
        /// Create SubDomain tree to ease message compression
        /// </summary>
        public static IDictionary<IDomainName, SubDomainName> CreateSubDomain(IEnumerable<IDomainName> sources) {

            if (sources == null) {
                throw new ArgumentException("sources");
            }
            
            // domains contains the first element of a domain name
            // sub => sub > domain > com > .
            // domain => domain > com > .
            //
            var domains = new Dictionary<string, SubDomainName>();

            // SubDomains contains the last matched element for a domain name
            // sub.domain.com => sub > domain > com > .
            // host.sub.domain.com => host > sub > domain > com > .
            //
            var subDomains = new Dictionary<IDomainName, SubDomainName>();

            // names contains remaining domain name that have to be treated
            // Name is split into SubDomains while iterating
            // host.sub.domain.com.
            // host.sub.domain.
            // host.sub.
            // host.
            // .
            var names = sources.ToDictionary(domainName => domainName, domainName => domainName.Name);
            bool hasDomain;
            do {

                hasDomain = false;

                foreach (IDomainName domainName in sources) {

                    // Get remaining domain name for this DomainName
                    string nameTemp = names[domainName];
                    if (string.IsNullOrEmpty(nameTemp)) {
                        continue;
                    }

                    Match m = Subdomain.Match(nameTemp);
                    if (m.Success) {

                        string name = m.Groups["SubDomain"].Value;
                        var subDomain = new SubDomainName {
                            Name = name,
                            Right = subDomains.ContainsKey(domainName) ? subDomains[domainName] : null
                        };

                        string fulldomain = subDomain.FullDomain();
                        if (!domains.ContainsKey(fulldomain)) {
                            domains.Add(fulldomain, subDomain);
                        }
                        subDomains[domainName] = domains[fulldomain];

                        nameTemp = Subdomain.Replace(nameTemp, "");
                        names[domainName] = nameTemp;
                        if (!string.IsNullOrEmpty(nameTemp)) {
                            hasDomain = true;
                        }
                    }
                }

            } while (hasDomain);

            return subDomains;
        }

        internal static void Init(ICompressionContext compression, IMessage message) {

            if (compression == null) {
                throw new ArgumentNullException("compression");
            }

            if (message == null) {
                throw new ArgumentNullException("message");
            }

            CompressionData data = (CompressionData)(compression.Data);

            var domains = new List<IDomainName>();
            CreateDomain(message, domains);
            SanitanizeDomain(domains);
            IDictionary<IDomainName, SubDomainName> subDomains = CreateSubDomain(domains);
            data.Domains = domains;
            data.SubDomains = subDomains;

        }


        public static void SanitanizeDomain(this List<IDomainName> domains) {

            if (domains == null) {
                throw new ArgumentNullException("domains");
            }

            foreach (IDomainName domain in domains) {
                SanitanizeDomain(domain);
            }
        }


        private static readonly Regex Sanitanize = new Regex(@"[^a-z0-9\-\.]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private static readonly Regex Sanitandot = new Regex(@"\.+", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        public static void SanitanizeDomain(IDomainName domain) {

            if (domain == null) {
                throw new ArgumentNullException("domain");
            }

            domain.Name = string.Format("{0}.", domain.Name);
            domain.Name = Sanitanize.Replace(domain.Name, "");
            domain.Name = Sanitandot.Replace(domain.Name, ".");

        }

    }
}